/*
 * Copyright 2014 http://Bither.net
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.bither.preference;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import net.bither.BitherApplication;

import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;

/**
 * A persistent cookie store which implements the Apache HttpClient
 * {@link CookieStore} interface. Cookies are stored and will persist on the
 * user's device between application sessions since they are serialized and
 * stored in {@link SharedPreferences}.
 * <p>
 * Instances of this class are designed to be used with
 * {@link AsyncHttpClient#setCookieStore}, but can also be used with a regular
 * old apache HttpClient/HttpContext if you prefer.
 */
public class PersistentCookieStore implements CookieStore {
	private static final String COOKIE_PREFS = "CookiePrefsFile";
	private static final String COOKIE_NAME_STORE = "names";
	private static final String COOKIE_NAME_PREFIX = "cookie_";

	private ConcurrentHashMap<String, Cookie> cookies;
	private SharedPreferences cookiePrefs;
	private static PersistentCookieStore persistentCookieStore = new PersistentCookieStore();

	public static PersistentCookieStore getInstance() {
		return persistentCookieStore;
	}

	/**
	 * Construct a persistent cookie store.
	 */
	private PersistentCookieStore() {
		reload();
	}

	@Override
	public void addCookie(Cookie cookie) {
		String name = cookie.getName();

		// Save cookie into local store, or remove if expired
		if (!cookie.isExpired(new Date())) {
			cookies.put(name, cookie);
		} else {
			cookies.remove(name);
		}

		// Save cookie into persistent store
		SharedPreferences.Editor prefsWriter = this.cookiePrefs.edit();
		prefsWriter.putString(COOKIE_NAME_STORE,
				TextUtils.join(",", cookies.keySet()));
		prefsWriter.putString(COOKIE_NAME_PREFIX + name,
				encodeCookie(new SerializableCookie(cookie)));
		prefsWriter.commit();
	}

	@Override
	public void clear() {
		// Clear cookies from local store
		cookies.clear();

		// Clear cookies from persistent store
		SharedPreferences.Editor prefsWriter = this.cookiePrefs.edit();
		for (String name : cookies.keySet()) {
			prefsWriter.remove(COOKIE_NAME_PREFIX + name);
		}
		prefsWriter.remove(COOKIE_NAME_STORE);
		prefsWriter.commit();
	}

	@Override
	public boolean clearExpired(Date date) {
		boolean clearedAny = false;
		SharedPreferences.Editor prefsWriter = this.cookiePrefs.edit();

		for (ConcurrentHashMap.Entry<String, Cookie> entry : cookies.entrySet()) {
			String name = entry.getKey();
			Cookie cookie = entry.getValue();
			if (cookie.isExpired(date)) {
				// Clear cookies from local store
				cookies.remove(name);

				// Clear cookies from persistent store
				prefsWriter.remove(COOKIE_NAME_PREFIX + name);

				// We've cleared at least one
				clearedAny = true;
			}
		}

		// Update names in persistent store
		if (clearedAny) {
			prefsWriter.putString(COOKIE_NAME_STORE,
					TextUtils.join(",", cookies.keySet()));
		}
		prefsWriter.commit();
		return clearedAny;
	}

	@Override
	public List<Cookie> getCookies() {
		return new ArrayList<Cookie>(cookies.values());
	}

	//
	// Cookie serialization/deserialization
	//

	protected String encodeCookie(SerializableCookie cookie) {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		try {
			ObjectOutputStream outputStream = new ObjectOutputStream(os);
			outputStream.writeObject(cookie);
		} catch (Exception e) {
			return null;
		}

		return byteArrayToHexString(os.toByteArray());
	}

	protected Cookie decodeCookie(String cookieStr) {
		byte[] bytes = hexStringToByteArray(cookieStr);
		ByteArrayInputStream is = new ByteArrayInputStream(bytes);
		Cookie cookie = null;
		try {
			ObjectInputStream ois = new ObjectInputStream(is);
			cookie = ((SerializableCookie) ois.readObject()).getCookie();
		} catch (Exception e) {
			e.printStackTrace();
		}

		return cookie;
	}

	// Using some super basic byte array <-> hex conversions so we don't have
	// to rely on any large Base64 libraries. Can be overridden if you like!
	protected String byteArrayToHexString(byte[] b) {
		StringBuffer sb = new StringBuffer(b.length * 2);
		for (byte element : b) {
			int v = element & 0xff;
			if (v < 16) {
				sb.append('0');
			}
			sb.append(Integer.toHexString(v));
		}
		return sb.toString().toUpperCase(Locale.US);
	}

	protected byte[] hexStringToByteArray(String s) {
		int len = s.length();
		byte[] data = new byte[len / 2];
		for (int i = 0; i < len; i += 2) {
			data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character
					.digit(s.charAt(i + 1), 16));
		}
		return data;
	}

	@SuppressLint("InlinedApi")
	public void reload() {
		// Load any previously stored cookies into the store
		this.cookiePrefs = BitherApplication.mContext.getSharedPreferences(
				COOKIE_PREFS, Context.MODE_MULTI_PROCESS);
		cookies = new ConcurrentHashMap<String, Cookie>();

		String storedCookieNames = this.cookiePrefs.getString(
				COOKIE_NAME_STORE, null);
		if (storedCookieNames != null) {
			String[] cookieNames = TextUtils.split(storedCookieNames, ",");
			for (String name : cookieNames) {
				String encodedCookie = this.cookiePrefs.getString(
						COOKIE_NAME_PREFIX + name, null);
				if (encodedCookie != null) {
					Cookie decodedCookie = decodeCookie(encodedCookie);
					if (decodedCookie != null) {
						cookies.put(name, decodedCookie);
					}
				}
			}

			// Clear out expired cookies
			clearExpired(new Date());
		}
	}

}